const TOKEN_TYPE = {
  KEYWORD: 'keyword',
  SYMBOL: 'symbol',
  IDENTIFIER: 'identifier',
  INT_CONST: 'integerConstant',
  STRING_CONST: 'stringConstant'
};

const KEY_WORD = {
  CLASS: 'class',
  METHOD: 'method',
  FUNCTION: 'function',
  CONSTRUCTOR: 'constructor',
  INT: 'int',
  BOOLEAN: 'boolean',
  CHAR: 'char',
  VOID: 'void',
  VAR: 'var',
  STATIC: 'static',
  FIELD: 'field',
  LET: 'let',
  DO: 'do',
  IF: 'if',
  ELSE: 'else',
  WHILE: 'while',
  RETURN: 'return',
  TRUE: 'true',
  FALSE: 'false',
  NULL: 'null',
  THIS: 'this'
};

const KEY_WORDS = Object.values(KEY_WORD);

const SYMBOL = ['{', '}', '(', ')', '[', ']', '.', ',', ';', '+', '-', '*', '/', '&', '|', '<', '>', '=', '~'];
const IDENTIFIER_PATTERN = /[a-z|A-Z|\d|_]/;

class JackTokenizer {
  constructor() {
    this.reset('');
    this.currentToken = {
      type: '',
      str: ''
    };
  }

  getClonedCurrentToken() {
    return { ...this.currentToken };
  }

  reset(inputString) {
    this.inputString = inputString;
    this.cursorEndPosition = inputString.length;
    this.cursor = 0;
  }

  hasMoreTokens() {
    this._ignoreInvalid();
    return this.cursor < this.cursorEndPosition && this.inputString[this.cursor];
  }

  advance() {
    if (!this.hasMoreTokens()) return false;
    this._catchNextToken();
  }

  toXmlLine() {
    const tag = this.currentToken.type;
    const content = this._escapeChar(this.currentToken.str);
    return `<${tag}> ${content} </${tag}>`;
  }

  _escapeChar(str) {
    if (typeof str === 'string') {
      return str.replace('&', '&amp;').replace('>', '&gt;').replace('<', '&lt;').replace('"', '&quot;');
    }
    return str;
  }

  _normalizeEndOfLine(inputString) {
    return inputString.replace(/\r\n/g, '\n');
  }

  _ignoreInvalid() {
    if (this.cursor >= this.cursorEndPosition) return;
    let lastCursor = -1;
    while (lastCursor !== this.cursor) {
      lastCursor = this.cursor;
      this._ignoreWhiteSpace();
      this._ignoreLineComments();
      this._ignoreBlockComments();
    }
  }

  _ignoreLineComments() {
    if (this.inputString[this.cursor] === '/' && this.inputString[this.cursor + 1] === '/') {
      this.cursor += 2;
      while (this.inputString[this.cursor] !== '\n') {
        this.cursor++;
      }
      this.cursor++;
    }
  }

  _ignoreBlockComments() {
    if (this.inputString[this.cursor] === '/' && this.inputString[this.cursor + 1] === '*') {
      this.cursor += 2;
      while (!(this.inputString[this.cursor] === '*' && this.inputString[this.cursor + 1] === '/')) {
        this.cursor++;
      }
      this.cursor += 2;
    }
  }

  _ignoreWhiteSpace() {
    while (/\s/.test(this.inputString[this.cursor])) {
      this.cursor++;
    }
  }

  _catchNextToken() {
    const head = this.inputString[this.cursor];
    const start = this.cursor;

    if (SYMBOL.includes(head)) {
      this.currentToken.type = TOKEN_TYPE.SYMBOL;
      this.currentToken.str = head;
      this.cursor++;
      return;
    }

    if (/\d/.test(head)) {
      while (/\d/.test(this.inputString[++this.cursor])) {}
      this.currentToken.type = TOKEN_TYPE.INT_CONST;
      this.currentToken.str = parseInt(this.inputString.slice(start, this.cursor));
      return;
    }

    if (head === '"') {
      while (this.inputString[++this.cursor] !== '"') {}
      this.currentToken.type = TOKEN_TYPE.STRING_CONST;
      this.currentToken.str = this.inputString.slice(start + 1, this.cursor);
      this.cursor++;
      return;
    }

    if (IDENTIFIER_PATTERN.test(head)) {
      while (IDENTIFIER_PATTERN.test(this.inputString[++this.cursor])) {}
      const token = this.inputString.slice(start, this.cursor);
      this.currentToken.str = token;
      this.currentToken.type = KEY_WORDS.includes(token) ? TOKEN_TYPE.KEYWORD : TOKEN_TYPE.IDENTIFIER;
      return;
    }

    throw new Error('invalid token: ', head);
  }

  _getTokenType(str) {
    if (KEY_WORDS.includes(str)) {
      return TOKEN_TYPE.KEYWORD;
    }
    if (str[0] === '"' && str[str.length - 1] === '"') {
      return TOKEN_TYPE.STRING_CONST;
    }
    if (/^\d+$/.test(str)) {
      return TOKEN_TYPE.INT_CONST;
    }
  }

  tokenType() {
    return this.currentToken.type;
  }

  keyWord() {
    return this.currentToken.str;
  }

  symbol() {
    return this.currentToken.str;
  }

  identifier() {
    return this.currentToken.str;
  }

  intVal() {
    return this.currentToken.str;
  }

  stringVal() {
    return this.currentToken.str;
  }
}

module.exports.JackTokenizer = JackTokenizer;
module.exports.TOKEN_TYPE = TOKEN_TYPE;
module.exports.KEY_WORD = KEY_WORD;
module.exports.SYMBOL = SYMBOL;
